package com.wr.hangzhounewlight.service;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.wr.hangzhounewlight.mapper.OrderMapper;
import com.wr.hangzhounewlight.mapper.RecipeMapper;
import com.wr.hangzhounewlight.obj.database.OrderEntity;
import com.wr.hangzhounewlight.obj.database.RecipeEntity;
import com.wr.hangzhounewlight.opcua.OpcUaClientManager;
import com.wr.hangzhounewlight.opcua.OpcUaUtil;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaMonitoredItem;
import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaMonitoredItem.DataValueListener;
import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaSubscription;
import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaSubscription.SubscriptionListener;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.stream.IntStream;

/**
 * @author yuanzhizhuo
 * createDate 2025/10/28
 */
@Slf4j
@Service
public class RecipeIssueListenerService {

    private final OpcUaClientManager opcUaClientManager;
    private final OrderMapper orderMapper;
    private final RecipeMapper recipeMapper;
    private final String opcUaUrl = "opc.tcp://frp-fog.com:48347";
    private final List<NodeId> listenerType01NodeIdList = List.of(
            new NodeId(1, "ChuiMo_M3_A_MESBatchSel")
    );
    private final List<NodeId> listenerType02NodeIdList = List.of(
            new NodeId(1, "ChuiMo_M3_A_MES_btn_loadRCP_001"),
            new NodeId(1, "ChuiMo_M3_A_MES_btn_loadRCP_002"),
            new NodeId(1, "ChuiMo_M3_A_MES_btn_loadRCP_003"),
            new NodeId(1, "ChuiMo_M3_A_MES_btn_loadRCP_004"),
            new NodeId(1, "ChuiMo_M3_A_MES_btn_loadRCP_005")
    );
    private static final Map<String, String> workshopMap = new HashMap<>();

    static {
        workshopMap.put("NongMo", "农膜");
        workshopMap.put("ChuiMo", "吹膜");
        workshopMap.put("BaoCai", "包材");
    }

    public RecipeIssueListenerService(OpcUaClientManager opcUaClientManager, OrderMapper orderMapper, RecipeMapper recipeMapper) {
        this.opcUaClientManager = opcUaClientManager;
        this.orderMapper = orderMapper;
        this.recipeMapper = recipeMapper;
    }

    private static final Map<String, String> T = new HashMap<>();

    static {
        IntStream.rangeClosed(1, 60).forEach(i -> T.put("主料" + i, "SER_RCP" + "_" + String.format("%03d", i)));
    }

    /**
     * TODO 没有考虑RCPName（recipeName）, 只考虑了WO（orderNo）
     */
    @PostConstruct
    private void addOpcUaNodeIdListener() {
        Optional<OpcUaClient> opcUaClientOptional = opcUaClientManager.getOpcUaClient(opcUaUrl);
        if (opcUaClientOptional.isPresent()) {
            OpcUaClient opcUaClient = opcUaClientOptional.get();
            List<NodeId> listenerNodeIdList = new ArrayList<>(listenerType01NodeIdList);
            listenerNodeIdList.addAll(listenerType02NodeIdList);
            try {
                List<NodeId> awakenNodeIdList = new ArrayList<>(listenerNodeIdList);
                OpcUaUtil.readValue(opcUaClient, awakenNodeIdList);
            } catch (ExecutionException | InterruptedException | TimeoutException e) {
                throw new RuntimeException(e);
            }
            List<DataValueListener> nodeIdListenerType01List = listenerType01NodeIdList.stream().map(nodeId ->
                    (DataValueListener) new MyDataValueListenerType01(opcUaClient, orderMapper)
            ).toList();
            List<DataValueListener> nodeIdListenerType02List = listenerType02NodeIdList.stream().map(nodeId ->
                    (DataValueListener) new MyDataValueListenerType02(opcUaClient, orderMapper, recipeMapper)
            ).toList();
            List<DataValueListener> listenerNodeIdCallback = new ArrayList<>(nodeIdListenerType01List);
            listenerNodeIdCallback.addAll(nodeIdListenerType02List);
            try {
                OpcUaUtil.subscribeDataValue(opcUaClient, new MySubscriptionListener(), listenerNodeIdList, listenerNodeIdCallback);
            } catch (UaException | ExecutionException | InterruptedException | TimeoutException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 监听器级别回调
     */
    private static class MySubscriptionListener implements SubscriptionListener {
        @Override
        public void onDataReceived(OpcUaSubscription subscription, List<OpcUaMonitoredItem> items, List<DataValue> values) {
            for (int i = 0; i < items.size(); i++) {
                OpcUaMonitoredItem item = items.get(i);
                DataValue value = values.get(i);
                log.info("配方下发, 监听器事件, 监听点位: {}发生变化, 新值: {}", item.getReadValueId().getNodeId(), value.getValue().getValue());
            }
        }
    }

    @Slf4j
    private static class MyDataValueListenerType01 implements DataValueListener {

        private final OpcUaClient opcUaClient;
        private final OrderMapper orderMapper;
        private String workshop;
        private String workshopCH;
        private String machine;
        private String screw;

        public MyDataValueListenerType01(OpcUaClient opcUaClient, OrderMapper orderMapper) {
            this.opcUaClient = opcUaClient;
            this.orderMapper = orderMapper;
        }

        /**
         * identifier中带有MESBatchSel关键字的节点监听器
         * <p>
         * 完整格式[workshop]_[machine]_[screw]_MESBatchSel
         * <p>
         * 根据触发节点的值从order表中查询组合单号
         * 检查组合单号是否符合要求
         * 将组合单号写入对应的TempWO节点
         * 检查是否写入成功
         */
        @Override
        public void onDataReceived(OpcUaMonitoredItem item, DataValue value) {
            NodeId nodeId = item.getReadValueId().getNodeId();
            String[] identifierSplit = String.valueOf(nodeId.getIdentifier()).split("_");
            workshop = identifierSplit[0];
            workshopCH = workshopMap.get(workshop);
            machine = identifierSplit[1];
            screw = identifierSplit[2];
            List<String> orderNoList = selectNotClosedOrderNo(Integer.parseInt(String.valueOf(value.getValue().getValue())));
            List<String> issueOrderNoList = checkDatabaseData(orderNoList);
            if (issueOrderNoList != null) {
                checkIssue(writeTempWO(issueOrderNoList), issueOrderNoList);
            }
        }

        /**
         * 从数据库查询未关闭的组合单号
         * <p>
         * 如果节点值是1, 就取1～5, 是2就取6～10, 以此类推, 每次取5条
         * 按照插入时间升序排序, 去重
         * <p>
         * TODO 没有查询出RCPName（recipeName）
         *
         * @param nodeValue 节点值
         * @return 未下发的配方列表
         */
        private List<String> selectNotClosedOrderNo(int nodeValue) {
            if (nodeValue == 0) return null;
            int start = (nodeValue - 1) * 5 + 1;
            return orderMapper.selectNotClosedOrderNo(workshopCH, machine, screw, start - 1, 5);
        }

        /**
         * 检查数据库数据是否符合要求
         * <p>
         * 检查是否为空
         * 检查是否不足5条, 不足则用空对象填充
         *
         * @param orderNoList 未关闭的组合单号
         * @return 5条组合单号
         */
        private List<String> checkDatabaseData(List<String> orderNoList) {
            if (orderNoList == null) {
                log.error("配方下发, 未找到对应的组合单号, 车间名称: {}, 机台: {}, 螺杆: {}", workshopCH, machine, screw);
                return null;
            }
            if (orderNoList.size() < 5) {
                log.warn("配方下发, 组合单号不足5条, 车间名称: {}, 机台: {}, 螺杆: {}, 组合单号: {}", workshopCH, machine, screw, orderNoList);
                int missingCount = 5 - orderNoList.size();
                orderNoList.addAll(Collections.nCopies(missingCount, ""));
                return orderNoList;
            }
            if (orderNoList.size() == 5) {
                log.info("配方下发, 在数据库中找到组合单号, 车间名称: {}, 机台: {}, 螺杆: {}, 组合单号: {}", workshopCH, machine, screw, orderNoList);
                return orderNoList;
            }
            log.error("配方下发, 从数据库中获取组合单号出现其他异常, 车间名称: {}, 机台: {}, 螺杆: {}", workshopCH, machine, screw);
            return null;
        }


        /**
         * 将组合单号缓存到HMI的TempWO节点
         *
         * @param issueOrderNoList 检查过的数据库记录
         * @return 缓存组合单号的节点id列表
         */
        private List<NodeId> writeTempWO(List<String> issueOrderNoList) {
            log.info("配方下发, 正在将组合单号缓存到HMI, 车间名称: {}, 机台: {}, 螺杆: {}, 组合单号: {}", workshopCH, machine, screw, issueOrderNoList);
            List<NodeId> targetNodeIdList = new ArrayList<>();
            for (int i = 0; i < issueOrderNoList.size(); i++) {
                String identifier = workshop + "_" + machine + "_" + screw + "_" + "TempWO_" + String.format("%03d", i + 1);
                NodeId targetNodeId = new NodeId(1, identifier);
                targetNodeIdList.add(targetNodeId);
                try {
                    OpcUaUtil.writeValue(opcUaClient, targetNodeId, issueOrderNoList.get(i));
                    log.info("配方下发, 已将组合单号缓存到HMI, 节点id: {}, 组合单号: {}", targetNodeId, issueOrderNoList.get(i));
                } catch (UaException e) {
                    log.error("配方下发, 缓存到HMI失败, 节点id: {}, 错误信息: {}", targetNodeId, e.getMessage());
                }
            }
            return targetNodeIdList;
        }

        /**
         * 校验HMI缓存的组合单号是否与数据库数据一致
         *
         * @param targetNodeIdList 缓存组合单号的节点id列表
         * @param orderNoList      检查过的组合单号列表
         */
        private void checkIssue(List<NodeId> targetNodeIdList, List<String> orderNoList) {
            try {
                Thread.sleep(2000);
                List<DataValue> dataValueList = OpcUaUtil.readValue(opcUaClient, targetNodeIdList);
                List<String> actValue = dataValueList.stream().map(dataValue -> String.valueOf(dataValue.getValue().getValue())).toList();
                if (actValue.equals(orderNoList)) {
                    log.info("配方下发, 校验通过, 数据库中的组合单号与HMI缓存一致");
                } else {
                    log.error("配方下发, 校验失败, 数据库中的组合单号与HMI缓存不一致, 期望: {}, 实际: {}", orderNoList, actValue);
                }
            } catch (ExecutionException | InterruptedException | TimeoutException e) {
                log.info("配方下发, 校验HMI缓存的组合单号失败, 错误信息: {}", e.getMessage());
            }
        }

    }

    @Slf4j
    private static class MyDataValueListenerType02 implements DataValueListener {

        private final OpcUaClient opcUaClient;
        private final OrderMapper orderMapper;
        private final RecipeMapper recipeMapper;
        private String workshop;
        private String machine;
        private String screw;
        private String suffix;

        public MyDataValueListenerType02(OpcUaClient opcUaClient, OrderMapper orderMapper, RecipeMapper recipeMapper) {
            this.opcUaClient = opcUaClient;
            this.orderMapper = orderMapper;
            this.recipeMapper = recipeMapper;
        }

        /**
         * identifier中带有btn_loadRCP关键字的节点监听器
         * <p>
         * 完整格式[workshop]_[machine]_[screw]_MES_btn_loadRCP_[suffix]
         * <p>
         * 重置确认ok按钮为false
         * 读取对应的TempWO节点, 将值写入对应的curWO节点
         * 重置表recipes中对应组合单号的is_check字段为0
         * 将recipes表中组合单号对应的料仓比例写入对应的SER_RCP节点
         * 检查是否写入成功
         * 重置触发节点值为false
         */
        @Override
        public void onDataReceived(OpcUaMonitoredItem item, DataValue value) {
            if (!Boolean.parseBoolean(String.valueOf(value.getValue().getValue()))) return;
            NodeId nodeId = item.getReadValueId().getNodeId();
            String[] identifierSplit = String.valueOf(nodeId.getIdentifier()).split("_");
            workshop = identifierSplit[0];
            machine = identifierSplit[1];
            screw = identifierSplit[2];
            suffix = identifierSplit[6];
            resetIsCheckButton();
            String orderNo = writeCurrentOrderNo();
            resetDatabaseOrderNoIsCheck(orderNo);
            OrderNoAndRecipesName orderNoAndRecipesName = readOrderNoAndRecipeName(item.getReadValueId().getNodeId());
            List<RecipeEntity> recipeEntityList = selectRecipes(orderNoAndRecipesName.orderNo(), orderNoAndRecipesName.recipesName());
            List<NodeId> nodeIdList = writeRatio(recipeEntityList);
            List<Double> ratioList = recipeEntityList.stream().map(RecipeEntity::getRatio).toList();
            checkIssue(nodeIdList, ratioList);
            try {
                OpcUaUtil.writeValue(opcUaClient, nodeId, false);
            } catch (UaException e) {
                log.error("配方下发, 下发失败, 重置节点值为false失败, 节点id: {}", nodeId, e);
            }
        }

        /**
         * 重置确认ok按钮为false
         */
        private void resetIsCheckButton() {
            String orderNoIsCheckNodeIdentifier = workshop + "_" + machine + "_" + screw + "_PF_OK";
            NodeId nodeId = new NodeId(1, orderNoIsCheckNodeIdentifier);
            try {
                OpcUaUtil.writeValue(opcUaClient, nodeId, false);
            } catch (UaException e) {
                log.info("配方下发, 重置确认ok按钮: {}失败, 错误信息: {}", nodeId, e.getMessage());
            }
        }

        /**
         * 写入当前组合单号
         *
         * @return 车间机台螺杆当前的组合单号
         */
        private String writeCurrentOrderNo() {
            String tempWOIdentifier = workshop + "_" + machine + "_" + screw + "_TempWO_" + suffix;
            NodeId tempWONodeId = new NodeId(1, tempWOIdentifier);
            String curWONodeIdentifier = workshop + "_" + machine + "_" + screw + "_" + "curWO";
            NodeId curWONodeId = new NodeId(1, curWONodeIdentifier);
            try {
                DataValue tempWOValue = opcUaClient.readValue(0.0, TimestampsToReturn.Both, tempWONodeId);
                String orderNo = String.valueOf(tempWOValue.getValue().getValue());
                OpcUaUtil.writeValue(opcUaClient, curWONodeId, orderNo);
                log.info("配方下发, 已写入当前组合单号到HMI, 节点id: {}, 组合单号: {}", curWONodeId, orderNo);
                return orderNo;
            } catch (UaException e) {
                log.error("配方下发, 写入当前组合单号到HMI失败, 节点id: {}, 错误信息: {}", curWONodeId, e.getMessage());
                return null;
            }
        }

        /**
         * 重置数据库中组合单号的recipe_is_check为0
         *
         * @param orderNo 组合单号
         */
        private void resetDatabaseOrderNoIsCheck(String orderNo) {
            LambdaUpdateWrapper<OrderEntity> updateWrapper = new LambdaUpdateWrapper<OrderEntity>()
                    .set(OrderEntity::getRecipeIsCheck, 0)
                    .eq(OrderEntity::getOrderNo, orderNo);
            orderMapper.update(updateWrapper);
        }

        /**
         * 获取监听节点对应的组合单号和配方名称
         *
         * @param nodeId 发生变换的节点id
         * @return 组合单号和配方名称
         */
        private OrderNoAndRecipesName readOrderNoAndRecipeName(NodeId nodeId) {
            String identifier = String.valueOf(nodeId.getIdentifier());
            String[] identifierSpilt = identifier.split("_");
            String nodeIdentifierPrefix = workshop + "_" + machine + "_" + screw + "_";
            NodeId orderNoNodeId = new NodeId(1, nodeIdentifierPrefix + "TempWO_" + identifierSpilt[identifierSpilt.length - 1]);
            NodeId recipesNameNodeId = new NodeId(1, nodeIdentifierPrefix + "TempRCPName_" + identifierSpilt[identifierSpilt.length - 1]);
            try {
                List<DataValue> dataValueList = OpcUaUtil.readValue(opcUaClient, List.of(orderNoNodeId, recipesNameNodeId));
                List<String> orderNoAndRecipesName = dataValueList.stream().map(dataValue -> String.valueOf(dataValue.getValue().getValue())).toList();
                return new OrderNoAndRecipesName(orderNoAndRecipesName.get(0), orderNoAndRecipesName.get(1));
            } catch (ExecutionException | InterruptedException | TimeoutException e) {
                log.error("配方下发, 读取组合单号和配方名称失败, 节点id: {}", nodeId, e);
                return new OrderNoAndRecipesName("", "");
            }
        }

        private record OrderNoAndRecipesName(String orderNo, String recipesName) {}

        /**
         * 从数据中获取组合单号和配方名称对应的料仓信息
         * <p>
         * 按照插入时间升序排序, 取未下发的60条数据
         * <p>
         * TODO 可能会使用到recipesName
         *
         * @param orderNo     组合单号
         * @param recipesName 配方名称
         * @return 包含料仓信息的实体列表
         */
        private List<RecipeEntity> selectRecipes(String orderNo, String recipesName) {
            return recipeMapper.selectRecipe(orderNo);
        }

        /**
         * 写入配方配比到HMI
         *
         * @param recipeEntityList 数据库中的配方列表
         * @return 写入配方配比的节点id列表
         */
        private List<NodeId> writeRatio(List<RecipeEntity> recipeEntityList) {
            List<NodeId> nodeIdList = recipeEntityList.stream().map(recipeEntity -> {
                String nodeIdentifierPrefix = workshop + "_" + machine + "_" + screw + "_";
                String nodeIdentifierSuffix = T.get(recipeEntity.getSilo());
                return new NodeId(1, nodeIdentifierPrefix + nodeIdentifierSuffix);
            }).toList();
            for (int i = 0; i < nodeIdList.size(); i++) {
                Double ratio = recipeEntityList.get(i).getRatio();
                NodeId nodeId = nodeIdList.get(i);
                try {
                    OpcUaUtil.writeValue(opcUaClient, nodeId, ratio);
                    log.info("配方下发, 已将配方比例下发到节点id: {}, 配方配比: {}", nodeId, ratio);
                } catch (UaException e) {
                    log.error("配方下发, 下发失败, 节点id: {}, 配方配比: {}", nodeId, ratio, e);
                }
            }
            return nodeIdList;
        }

        /**
         * 检查节点中的配方配比是否与数据库中的配方配比一致
         *
         * @param targetNodeIdList 写入配方配比的节点id列表
         * @param ratioList        配方配比列表
         */
        private void checkIssue(List<NodeId> targetNodeIdList, List<Double> ratioList) {
            try {
                Thread.sleep(5000);
                List<DataValue> dataValueList = OpcUaUtil.readValue(opcUaClient, targetNodeIdList);
                List<Double> actValue = dataValueList.stream().map(dataValue ->
                        Double.parseDouble(String.format("%.3f", Double.parseDouble(String.valueOf(dataValue.getValue().getValue()))))
                ).toList();
                if (actValue.equals(ratioList)) {
                    log.info("配方下发, 校验成功");
                } else {
                    log.error("配方下发, 校验失败, 节点id: {}, 配方配比: {}, 实际配比: {}", targetNodeIdList, ratioList, actValue);
                }
            } catch (ExecutionException | InterruptedException | TimeoutException e) {
                log.error("配方下发, 检查配方配比失败, 节点id: {}, 配方配比: {}", targetNodeIdList, ratioList, e);
            }
        }

    }

}
